/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package view;

/**
 *
 * @author Pedrom
 */

import java.awt.*;
import java.beans.*;
import java.util.HashMap;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.text.*;

/**
 *  This class will display line numbers for a related text component. The text
 *  component must use the same line height for each line. TextLineNumber
 *  supports wrapped lines and will highlight the line number of the current
 *  line in the text component.
 *
 *  This class was designed to be used as a component added to the row header
 *  of a JScrollPane.
 */
public class TextLineNumber extends JPanel
        implements CaretListener, DocumentListener, PropertyChangeListener
{
        public final static float LEFT = 0.0f;
        public final static float CENTER = 0.5f;
        public final static float RIGHT = 1.0f;

        private final static Border OUTER = new MatteBorder(0, 0, 0, 2, Color.GRAY);

        private final static int HEIGHT = Integer.MAX_VALUE - 1000000;

        //  Text component this TextTextLineNumber component is in sync with

        private JTextComponent component;

        //  Properties that can be changed

        private boolean updateFont;
        private int borderGap;
        private Color currentLineForeground;
        private float digitAlignment;
        private int minimumDisplayDigits;

        //  Keep history information to reduce the number of times the component
        //  needs to be repainted

    private int lastDigits;
    private int lastHeight;
    private int lastLine;

        private HashMap<String, FontMetrics> fonts;

        /**
         *      Create a line number component for a text component. This minimum
         *  display width will be based on 3 digits.
         *
         *  @param component  the related text component
         */
        public TextLineNumber(JTextComponent component)
        {
                this(component, 3);
        }

        /**
         *      Create a line number component for a text component.
         *
         *  @param component  the related text component
         *  @param minimumDisplayDigits  the number of digits used to calculate
         *                               the minimum width of the component
         */
        public TextLineNumber(JTextComponent component, int minimumDisplayDigits)
        {
                this.component = component;

                setFont( component.getFont().deriveFont(Font.BOLD) );

                setBorderGap( 5 );
                setCurrentLineForeground( Color.RED );
                setDigitAlignment( RIGHT );
                setMinimumDisplayDigits( minimumDisplayDigits );

                component.getDocument().addDocumentListener(this);
                component.addCaretListener( this );
                component.addPropertyChangeListener("font", this);
        }

        /**
         *  Gets the update font property
         *
         *  @return the update font property
         */
        public boolean getUpdateFont()
        {
                return updateFont;
        }

        /**
         *  Set the update font property. Indicates whether this Font should be
         *  updated automatically when the Font of the related text component
         *  is changed.
         *
         *  @param updateFont  when true update the Font and repaint the line
         *                     numbers, otherwise just repaint the line numbers.
         */
        public void setUpdateFont(boolean updateFont)
        {
                this.updateFont = updateFont;
        }

        /**
         *  Gets the border gap
         *
         *  @return the border gap in pixels
         */
        public int getBorderGap()
        {
                return borderGap;
        }

        /**
         *  The border gap is used in calculating the left and right insets of the
         *  border. Default value is 5.
         *
         *  @param borderGap  the gap in pixels
         */
        public void setBorderGap(int borderGap)
        {
                this.borderGap = borderGap;
                Border inner = new EmptyBorder(0, borderGap, 0, borderGap);
                setBorder( new CompoundBorder(OUTER, inner) );
                lastDigits = 0;
                setPreferredWidth();
        }

        /**
         *  Gets the current line rendering Color
         *
         *  @return the Color used to render the current line number
         */
        public Color getCurrentLineForeground()
        {
                return currentLineForeground == null ? getForeground() : currentLineForeground;
        }

        /**
         *  The Color used to render the current line digits. Default is Coolor.RED.
         *
         *  @param currentLineForeground  the Color used to render the current line
         */
        public void setCurrentLineForeground(Color currentLineForeground)
        {
                this.currentLineForeground = currentLineForeground;
        }

        /**
         *  Gets the digit alignment
         *
         *  @return the alignment of the painted digits
         */
        public float getDigitAlignment()
        {
                return digitAlignment;
        }

        /**
         *  Specify the horizontal alignment of the digits within the component.
         *  Common values would be:
         *  <ul>
         *  <li>TextLineNumber.LEFT
         *  <li>TextLineNumber.CENTER
         *  <li>TextLineNumber.RIGHT (default)
         *      </ul>
         *  @param currentLineForeground  the Color used to render the current line
         */
        public void setDigitAlignment(float digitAlignment)
        {
                this.digitAlignment =
                        digitAlignment > 1.0f ? 1.0f : digitAlignment < 0.0f ? -1.0f : digitAlignment;
        }

        /**
         *  Gets the minimum display digits
         *
         *  @return the minimum display digits
         */
        public int getMinimumDisplayDigits()
        {
                return minimumDisplayDigits;
        }

        /**
         *  Specify the mimimum number of digits used to calculate the preferred
         *  width of the component. Default is 3.
         *
         *  @param minimumDisplayDigits  the number digits used in the preferred
         *                               width calculation
         */
        public void setMinimumDisplayDigits(int minimumDisplayDigits)
        {
                this.minimumDisplayDigits = minimumDisplayDigits;
                setPreferredWidth();
        }

        /**
         *  Calculate the width needed to display the maximum line number
         */
        private void setPreferredWidth()
        {
                Element root = component.getDocument().getDefaultRootElement();
                int lines = root.getElementCount();
                int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits);

                //  Update sizes when number of digits in the line number changes

                if (lastDigits != digits)
                {
                        lastDigits = digits;
                        FontMetrics fontMetrics = getFontMetrics( getFont() );
                        int width = fontMetrics.charWidth( '0' ) * digits;
                        Insets insets = getInsets();
                        int preferredWidth = insets.left + insets.right + width;

                        Dimension d = getPreferredSize();
                        d.setSize(preferredWidth, HEIGHT);
                        setPreferredSize( d );
                        setSize( d );
                }
        }

        /**
         *  Draw the line numbers
         */
        @Override
        public void paintComponent(Graphics g)
        {
                super.paintComponent(g);

                //      Determine the width of the space available to draw the line number

                FontMetrics fontMetrics = component.getFontMetrics( component.getFont() );
                Insets insets = getInsets();
                int availableWidth = getSize().width - insets.left - insets.right;

                //  Determine the rows to draw within the clipped bounds.

                Rectangle clip = g.getClipBounds();
                int rowStartOffset = component.viewToModel( new Point(0, clip.y) );
                int endOffset = component.viewToModel( new Point(0, clip.y + clip.height) );

                while (rowStartOffset <= endOffset)
                {
                        try
            {
                        if (isCurrentLine(rowStartOffset))
                                g.setColor( getCurrentLineForeground() );
                        else
                                g.setColor( getForeground() );

                        //  Get the line number as a string and then determine the
                        //  "X" and "Y" offsets for drawing the string.

                        String lineNumber = getTextLineNumber(rowStartOffset);
                        int stringWidth = fontMetrics.stringWidth( lineNumber );
                        int x = getOffsetX(availableWidth, stringWidth) + insets.left;
                                int y = getOffsetY(rowStartOffset, fontMetrics);
                        g.drawString(lineNumber, x, y);

                        //  Move to the next row

                        rowStartOffset = Utilities.getRowEnd(component, rowStartOffset) + 1;
                        }
                        catch(Exception e) {}
                }
        }

        /*
         *  We need to know if the caret is currently positioned on the line we
         *  are about to paint so the line number can be highlighted.
         */
        private boolean isCurrentLine(int rowStartOffset)
        {
                int caretPosition = component.getCaretPosition();
                Element root = component.getDocument().getDefaultRootElement();

                if (root.getElementIndex( rowStartOffset ) == root.getElementIndex(caretPosition))
                        return true;
                else
                        return false;
        }

        /*
         *      Get the line number to be drawn. The empty string will be returned
         *  when a line of text has wrapped.
         */
        protected String getTextLineNumber(int rowStartOffset)
        {
                Element root = component.getDocument().getDefaultRootElement();
                int index = root.getElementIndex( rowStartOffset );
                Element line = root.getElement( index );

                if (line.getStartOffset() == rowStartOffset)
                        return String.valueOf(index + 1);
                else
                        return "";
        }

        /*
         *  Determine the X offset to properly align the line number when drawn
         */
        private int getOffsetX(int availableWidth, int stringWidth)
        {
                return (int)((availableWidth - stringWidth) * digitAlignment);
        }

        /*
         *  Determine the Y offset for the current row
         */
        private int getOffsetY(int rowStartOffset, FontMetrics fontMetrics)
                throws BadLocationException
        {
                //  Get the bounding rectangle of the row

                Rectangle r = component.modelToView( rowStartOffset );
                int lineHeight = fontMetrics.getHeight();
                int y = r.y + r.height;
                int descent = 0;

                //  The text needs to be positioned above the bottom of the bounding
                //  rectangle based on the descent of the font(s) contained on the row.

                if (r.height == lineHeight)  // default font is being used
                {
                        descent = fontMetrics.getDescent();
                }
                else  // We need to check all the attributes for font changes
                {
                        if (fonts == null)
                                fonts = new HashMap<String, FontMetrics>();

                        Element root = component.getDocument().getDefaultRootElement();
                        int index = root.getElementIndex( rowStartOffset );
                        Element line = root.getElement( index );

                        for (int i = 0; i < line.getElementCount(); i++)
                        {
                                Element child = line.getElement(i);
                                AttributeSet as = child.getAttributes();
                                String fontFamily = (String)as.getAttribute(StyleConstants.FontFamily);
                                Integer fontSize = (Integer)as.getAttribute(StyleConstants.FontSize);
                                String key = fontFamily + fontSize;

                                FontMetrics fm = fonts.get( key );

                                if (fm == null)
                                {
                                        Font font = new Font(fontFamily, Font.PLAIN, fontSize);
                                        fm = component.getFontMetrics( font );
                                        fonts.put(key, fm);
                                }

                                descent = Math.max(descent, fm.getDescent());
                        }
                }

                return y - descent;
        }

//
//  Implement CaretListener interface
//
        @Override
        public void caretUpdate(CaretEvent e)
        {
                //  Get the line the caret is positioned on

                int caretPosition = component.getCaretPosition();
                Element root = component.getDocument().getDefaultRootElement();
                int currentLine = root.getElementIndex( caretPosition );

                //  Need to repaint so the correct line number can be highlighted

                if (lastLine != currentLine)
                {
                        repaint();
                        lastLine = currentLine;
                }
        }

//
//  Implement DocumentListener interface
//
        @Override
        public void changedUpdate(DocumentEvent e)
        {
                documentChanged();
        }

        @Override
        public void insertUpdate(DocumentEvent e)
        {
                documentChanged();
        }

        @Override
        public void removeUpdate(DocumentEvent e)
        {
                documentChanged();
        }

        /*
         *  A document change may affect the number of displayed lines of text.
         *  Therefore the lines numbers will also change.
         */
        private void documentChanged()
        {
                //  Preferred size of the component has not been updated at the time
                //  the DocumentEvent is fired

                SwingUtilities.invokeLater(new Runnable()
                {
                        public void run()
                        {
                        int preferredHeight = component.getPreferredSize().height;

                                //  Document change has caused a change in the number of lines.
                                //  Repaint to reflect the new line numbers

                        if (lastHeight != preferredHeight)
                        {
                                setPreferredWidth();
                                repaint();
                                lastHeight = preferredHeight;
                        }
                        }
                });
        }

//
//  Implement PropertyChangeListener interface
//
        @Override
        public void propertyChange(PropertyChangeEvent evt)
        {
                if (evt.getNewValue() instanceof Font)
                {
                        if (updateFont)
                        {
                                Font newFont = (Font) evt.getNewValue();
                                setFont(newFont);
                                lastDigits = 0;
                                setPreferredWidth();
                        }
                        else
                        {
                                repaint();
                        }
                }
        }
}